/*
 * To veranderOps this template, choose Tools | Templates
 * and open the template in the editor.
 */
package newServer;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.*;

/**
 *
 * @author Dieter Van der Stock
 */
public class Server implements Runnable {

    private InetAddress hostAdres;
    private int poort;
    private ServerSocketChannel serverKanaal; //hiermee aanvaarden we connecties
    private Selector selector; //de 'core' van alles, houdt alle connecties bij
    private ByteBuffer leesBuffer = ByteBuffer.allocate(2048); //de buffer waar we gaan inlezen
    private EchoWorker werker; //de klasse die gaat bepalen wat er moet geantwoord worden ed
    private List updateLijst = new LinkedList(); //gelinkte lijst met veranderingen die moeten gedaan worden
    private Map dataMap = new HashMap(); //gaat bijhouden welke data (in een queue) hoort bij welk afzenderSocket om te verzenden
    private Map poolMap = new HashMap(); //linkt de namen van pools met het poolobject
    
    //houdt alle verbonden verbindingen bij, 
    // mooi voor overzicht en handig voor naar 1 specifieke nickname te sturen
    private Map alleClients = new HashMap(); 
    
    //enkele poolnamen definiëren
    //public static String ALL = "alleclients"; // is op de moment nog de arraylist alleClients
    public static String INGELOGD = "ingelogdeclients";
    public static String NIETINGELOGD = "nietingelogdeclients";

    public Server(InetAddress hostAdres, int poort, EchoWorker werker) throws IOException {
        this.hostAdres = hostAdres;
        this.poort = poort;
        this.selector = this.initSelector();
        this.werker = werker;
    }

    public void stuurBericht(SocketChannel socketKanaal, byte[] data) {
        //we willen een bericht naar een bepaalde afzenderSocket versturen. Hiervoor veranderen we eerst de OPS van die afzenderSocket in WRITE
        synchronized (this.updateLijst) {
            //dit doen we door toe te voegen aan een veranderingenlijst
            this.updateLijst.add(new updateOpsAanvraag(socketKanaal, updateOpsAanvraag.CHANGEOPS, SelectionKey.OP_WRITE));

            // Daarna koppelen we de data die we willen schrijven met het afzenderSocket waarnaar we willen schrijven in de dataMap
            //CHAT: mss moet ik hier al een lijst van sockets meekrijgen ofzo als parameter dat dan de pool voorstelt?
            synchronized (this.dataMap) {
                List queue = (List) this.dataMap.get(socketKanaal);
                if (queue == null) { //als er nog geen queue van data voor dat afzenderSocket bestaat maken we er een
                    queue = new ArrayList();
                    this.dataMap.put(socketKanaal, queue);
                }
                queue.add(ByteBuffer.wrap(data)); //we steken de data die intussen naar die client moet worden gestuurd in de queue, die ook in de datamap staat
            }
        }

        // We maken de selector wakker zodat de updates kunnen doorgevoerd worden
        // (dan gaat de run zien dat er write-keys zijn, daaropvolgend de verzendData() oproepen en de dataMap van hierboven afhandelen
        this.selector.wakeup();
    }

    public void run() {
        System.out.println("Server succesvol opgestart, we luisteren op poort " + poort + " voor binnenkomende connecties en data.");
        while (true) {
            try {
                // Nieuwe veranderingen doorvoeren
                synchronized (this.updateLijst) {
                    Iterator updates = this.updateLijst.iterator();
                    while (updates.hasNext()) {
                        //elke verandering steken we in een updateOpsAanvraag
                        updateOpsAanvraag veranderOps = (updateOpsAanvraag) updates.next();
                        switch (veranderOps.type) {
                            case updateOpsAanvraag.CHANGEOPS:
                                SelectionKey key = veranderOps.socketKanaal.keyFor(this.selector);
                                key.interestOps(veranderOps.ops);
                        }
                    }
                    this.updateLijst.clear();
                }

                // We wachten tot er iets gebeurt (select() gaat blocken tot het zover is)
                this.selector.select();

                // We vragen een set van alle events (keys) op en behandelen ze één voor één
                Iterator selectedKeys = this.selector.selectedKeys().iterator();
                while (selectedKeys.hasNext()) {
                    SelectionKey key = (SelectionKey) selectedKeys.next();
                    selectedKeys.remove();
                    
                    //als de key om één of andere reden niet deugd, skippen we deze iteratie
                    if (!key.isValid()) {
                        continue;
                    }

                    // Zien welk soort event we te pakken hebben in onze key en de juiste procedure oproepen
                    if (key.isAcceptable()) {
                        this.aanvaardConnectie(key); //nieuwe connectieaanvraag
                    } 
                    else if (key.isReadable()) {
                        this.aanvaardData(key); //we krijgen data binnen
                    } 
                    else if (key.isWritable()) {
                        this.verzendData(key); //we kunnen data wegschrijven naar een kanaal (er is data beschikbaar, zie 'stuurbericht'
                    }
                }
            } catch (Exception e) {
                System.out.println("Er is een fout opgetreden in run(): " + e.getMessage());
                e.printStackTrace();
            }
        }
    }

    private void aanvaardConnectie(SelectionKey key) throws IOException {
        // Als er een verbinding staat te wachten, betekent dat dat het kanaal ons serverkanaal moet zijn
        ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();

        // Aanvaard de verbinding en maak het non-blocking
        SocketChannel socketKanaal = serverSocketChannel.accept();
        //Socket socket = afzenderSocket.afzenderSocket();
        socketKanaal.configureBlocking(false);
        
        //deze socket zoiezo toevoegen aan onze collectie verbonden clients
        //die 'naam', zijnde hier het adres van de client, wordt in later stadium vervangen door de nickname
        alleClients.put(socketKanaal.socket().getRemoteSocketAddress().toString(), socketKanaal);
        //voegClientToeAanPool(ALL,afzenderSocket.socket().getRemoteSocketAddress().toString(), afzenderSocket);

        // We registreren onze nieuwe connectie en laten blijken dat we geïntresseerd zijn in leesbare data
        socketKanaal.register(this.selector, SelectionKey.OP_READ);
    }

    private void aanvaardData(SelectionKey key) throws IOException {
        SocketChannel afzenderSocket = (SocketChannel) key.channel();

        // Voor de zekerheid onze leesbuffer leegmaken voor we er iets nieuw insteken
        this.leesBuffer.clear();

        // Proberen data te lezen van de afzenderSocket
        int aantalBytesGelezen;
        try {
            aantalBytesGelezen = afzenderSocket.read(this.leesBuffer);
        } catch (IOException e) {
            System.out.println("Bij een client liep er plots iets mis. We cancellen de aanvraag en sluiten het kanaal naar " + afzenderSocket.socket().getRemoteSocketAddress());
            key.cancel();
            alleClients.remove(afzenderSocket);
            afzenderSocket.close();
            return;
        }

        if (aantalBytesGelezen == -1) {
            System.out.println("Een client heeft zijn verbinding afgesloten. We cancellen de aanvraag en sluiten het kanaal naar " + afzenderSocket.socket().getRemoteSocketAddress());
            alleClients.remove(afzenderSocket);
            key.channel().close();
            key.cancel();
            return;
        }

        String msg = new String(leesBuffer.array(), 0, aantalBytesGelezen);
        System.out.println("We kregen een bericht van " + afzenderSocket.socket().getRemoteSocketAddress() + "-> " + msg);
        
        //het binnengekregen bericht naar de werker sturen, die zal het wel verder behandelen
        this.werker.verwerkData(this,afzenderSocket,this.leesBuffer.array(),aantalBytesGelezen);
        
        /*
        // CHAT: hier lopen we de juiste pool af!! Hier een functie die de juiste lijst van sockets teruggeeft?
        Iterator socketIt = alleClients.iterator();
        System.out.println("We sturen dit door naar: ");
        while(socketIt.hasNext()) {
            SocketChannel ontvanger = (SocketChannel)socketIt.next();
            System.out.println("- " + ontvanger.socket().getRemoteSocketAddress());
            this.werker.verwerkData(this, ontvanger, this.leesBuffer.array(), aantalBytesGelezen);
            //socketIt.remove(); //mag ni verwijderd worden natuurlijk.. duh
        }
         * */
    }

    private void verzendData(SelectionKey key) throws IOException {
        SocketChannel socketKanaal = (SocketChannel) key.channel();

        //we handelen de datamap af die in 'stuurbericht' wordt toegevoegd
        synchronized (this.dataMap) {
            List queue = (List) this.dataMap.get(socketKanaal);

            // blijven schrijven tot heel de queue voor dat afzenderSocket leeg is
            while (!queue.isEmpty()) {
                ByteBuffer buf = (ByteBuffer) queue.get(0);
                System.out.println("We sturen " + "een bericht" + " naar " + socketKanaal.socket().getRemoteSocketAddress());
                socketKanaal.write(buf);
                if (buf.remaining() > 0) {
                    //of tot dat de socketbuffer van het OS vol zit
                    break;
                }
                queue.remove(0);
            }

            if (queue.isEmpty()) {
                //alles is geschreven, we zetten het terug naar read (hier niet met updates ofzo, want het gebeurt in dezelfde thread
                key.interestOps(SelectionKey.OP_READ);
            }
        }
    }

    private Selector initSelector() throws IOException {
        // We maken een selector aan 
        Selector socketSelector = Selector.open();

        // We maken het serverkanaal aan en configureren het als Non-Blocking
        this.serverKanaal = ServerSocketChannel.open();
        serverKanaal.configureBlocking(false);

        // We binden het kanaal tot het juiste adres en poort
        serverKanaal.socket().bind(new InetSocketAddress(this.hostAdres, this.poort));

        // We registreren dit serverkanaal bij de selector waarbij we laten blijken dat we
        // geïntresseerd zijn in binnenkomende connecties
        serverKanaal.register(socketSelector, SelectionKey.OP_ACCEPT);

        return socketSelector;
    }
    
    public void voegClientToeAanPool(String poolNaam, String nickname, SocketChannel socketKanaal) {
        //pool die is opgegeven bestaat al: we voegen de client toe
        if(poolMap.containsKey(poolNaam)) {
            //we nemen de desbetreffende pool eruit, voegen de client toe en steken de pool terug
            Pool pool = (Pool) poolMap.remove( poolMap.get(poolNaam) );
            pool.voegClientToe(new Client(nickname,socketKanaal));
            poolMap.put(poolNaam, pool);
        }
        //opgegeven pool bestaat nog niet: we maken hem aan, voegen die client toe, en steken hem in de map
        else {
            Pool nieuwePool = new Pool(poolNaam);
            nieuwePool.voegClientToe(new Client(nickname,socketKanaal));
            poolMap.put(poolNaam, nieuwePool);
        }
    }

    public void verwijderClientUitPool(String poolNaam, String nickname) {
        //pool die is opgegeven bestaat: we verwijderen de client. Zoniet, no harm done
        if(poolMap.containsKey(poolNaam)) {
            //we nemen de desbetreffende pool eruit, verwijderen de client en steken de pool terug
            Pool pool = (Pool) poolMap.remove( poolMap.get(poolNaam) );
            pool.verwijderClient(nickname);
            poolMap.put(poolNaam, pool);
        }
    }
    
    public void verwijderPool(String poolNaam) {
        poolMap.remove(poolNaam);
    }
    
    public Pool getPool(String poolNaam) {
        return (Pool)poolMap.get(poolNaam);
    }
    
    public List getNicknamesInPool(String poolNaam) {
        return ((Pool)poolMap.get(poolNaam)).getNicknames();
    }
    
    public List getSocketsInPool(String poolNaam) {
        return ((Pool)poolMap.get(poolNaam)).getSockets();
    }
    
    public void stuurBerichtNaarClient(String nickname, String bericht) {
        //als we zo'n client hebben, sturen we ernaar. Zoniet zal die wrs niet meer verbonden zijn
        //en negeren we het bericht
        if(alleClients.containsKey(nickname)) {
            stuurBericht((SocketChannel)alleClients.get(nickname), bericht.getBytes());
        }
    }
    
    public void stuurBerichtNaarPool(String poolNaam, String bericht) {
        //alle sockets uit die pool halen
        ArrayList sockets = ((Pool)poolMap.get(poolNaam)).getSockets();
        
        //voor elk van die sockets het stuurBericht oproepen (hopelijk werkt dit)
        Iterator it = sockets.iterator();
        while(it.hasNext()) {
            stuurBericht((SocketChannel)it.next(), bericht.getBytes());
        }
    }
    
    public static void main(String[] args) {
        try {
            System.out.println("Welkom op onze server.");
            EchoWorker werker = new EchoWorker();
            new Thread(werker).start();
            new Thread(new Server(null, 9090, werker)).start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
